最後他起身,用一種不很確定的聲音,告訴我他要回去了。
「怎麼回事?」
「沒有事件會傳過來的。所有的事件都被stopPropagation
給擋住了!」~節錄自《The Great Svelte:第五章》
self
stopPropagation
昨天的內容中,我們介紹了在 Svelte 專案裡使用事件修飾的語法,並用 HTML 內建的元素 <dialog>
做出了互動視窗 (Modal) 的雛型。今天就讓我們把互動視窗的完整功能實做出來。
首先來到我們的 Modal.svelte
,將預設開啟的程式碼修掉。接著說明一下該怎麼設計互動視窗 (Modal) 的開啟和關閉邏輯。我的想法是,既然互動視窗 (Modal.svelte
) 是掛載在主要元件 (App.svelte
) 下的其中一個子元件 (child component),又會跟著主要元件的狀態改變而開啟,那我們就把控制互動視窗 (Modal) 開啟與否的狀態當作一個變數,同樣也儲存到主要元件 (App.svelte
) 當中吧。
先來重新寫過我們的 Modal.svelte
:
/src/lib/Modal.svelte
<script>
import { createEventDispatcher } from "svelte";
export let showModal;
let dialog;
const dispatch = createEventDispatcher();
const handleClose = () => dispatch('closeModal');
setTimeout(() => {
dialog = document.querySelector('dialog');
// 刪除預設開啟的程式碼 dialog.showModal();
}, 1);
$: if (dialog && showModal) dialog.showModal();
$: if (dialog && !showModal) dialog.close();
</script>
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
<dialog on:close={handleClose}>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div>
<div class="title">
<h2>Forbidden</h2>
<small><em>adjective</em> for·bid·den \fər-ˈbi-dᵊn \</small>
</div>
<hr />
<div class="content">
<p>
You have no privilidge to enter domain beyond 87. Please register
as member so we can sent you some spam email.
</p>
</div>
<hr />
<!-- svelte-ignore a11y-autofocus -->
<div class="footer">
<button autofocus on:click={handleClose}>close modal</button>
</div>
</div>
</dialog>
第二行:import { createEventDispatcher } from "svelte";
引入 createEventDispatcher
為客製化事件做準備。
第三行:export let showModal;
如同先前提到的,我們需要用一個主要元件 (App.svelte
)當中的變數來表達互動視窗 (Modal) 開啟與否。同時我們也需要在代表互動視窗的子元件 (Modal.svelte
) 準備一個變數,接收來自主要元件的狀態。而這個重要的角色就用showModal
這個變數來擔任。
第六行:const dispatch = createEventDispatcher();
初始化一個事件產生器。
第七行:const handleClose = () => dispatch('closeModal');
宣告一個函式。這個函式會作為事件處理器,向主要元件 (App.svelte
) 傳遞 closeModal
這個客製化事件。
第十一行:// 刪除預設開啟的程式碼 dialog.showModal();
我們再昨天的程式碼當中寫下這一行,讓互動式窗 (Modal) 預設開啟。今天我們要讓互動視窗 (Modal) 有條件地開啟,所以記得把這一行刪掉。
第十四行:$: if (dialog && showModal) dialog.showModal();
互動視窗 (Modal) 開啟的條件是:當 Javascript 的變數 dialog
開始代表 HTML 元素 <dialog>
,並且 showModal
為 true
時。利用呼叫 showModal()
這個內建的函式來開啟互動式窗 (Modal)。
第十五行:$: if (dialog && !showModal) dialog.close();
互動視窗 (Modal) 關閉的條件是:當 Javascript 的變數 dialog
開始代表 HTML 元素 <dialog>
,並且 showModal
為 false
時。利用呼叫 close()
這個內建的函式來關閉互動式窗 (Modal)。
第十九行:<dialog on:close={handleClose}>
當 dialog
因為使用者按下【Esc】鍵而被關掉時,記得向主要元件 (App.svelte
) 傳遞 closeModal
事件。
第三十七行:<button autofocus on:click={handleClose}>close modal</button>
當這個 <button>
被按下時,也記得向主要元件 (App.svelte
) 傳遞 closeModal
事件。
接著來到主要元件 App.svelte
,我們要設定何時應該開啟互動視窗 (Modal),以及接收來自互動視窗的 closeModal
事件,以便即時更新狀態。
/src/App.svelte
<!-- 在 Javascript 當中 import Counter -->
<script>
import Counter from './lib/Counter.svelte';
import Modal from './lib/Modal.svelte';
let count = 87;
let showModal = false;
let someState = 'TheGreatSvelte';
const sparkle = (text) => {
const sparkles = ['★', '☆', '✧', '✪'];
const randomSparkles = () => sparkles[Math.floor(Math.random() * sparkles.length)];
const sparkledText = text.split('').reduce((a, c) => a + randomSparkles() + c, '');
return sparkledText;
}
const href = 'https://ithelp.ithome.com.tw/users/20120178/ironman/7031';
const handleClick = (e) => {
console.log(e);
const tobeCount = count + e.detail;
if (!(tobeCount > 87)) count = tobeCount;
else showModal = true;
}
</script>
<main>
<!-- 在 HTML 當中直接嵌入 Counter -->
<Counter {count} on:changeCount={handleClick}/>
<p class='comment'>Check out <a {href}>Svelte Tutorial</a>, the awesome article powered by {sparkle(someState)}!</p>
</main>
<Modal {showModal} on:closeModal={() => showModal = false}/>
第四行:import Modal from './lib/Modal.svelte';
引入我們需要的 Svelte 元件,也就是 Modal.svelte
。
第六行:let count = 87;
宣告變數 count
,並將初始值設定在 87
。
第七行:let showModal = false;
宣告變數 showModal
,並將初始值設定為 false
。這個變數就是在主要元件 (App.svelte
) 當中,要用來控制互動視窗 (Modal) 開啟與否的變數。
第十九行:const handleClick = (e) => {
如同在第 13 天當中實作的,利用 handleClick
這個事件處理器去處理來自 <Counter>
的事件。
第二十一行:const tobeCount = count + e.detail;
只不過今天我們要給 count
做一個限制。先在處理器內宣告一個變數 tobeCount
,利用這個變數預判 count
將要發生的變化。
第二十二行:if (!(tobeCount > 87)) count = tobeCount;
如果 count
將要變成一個不超過 87
的數字,那麼就讓這個變化發生吧。
第二十三行:else showModal = true;
否則的話,不僅不讓這個變化發生,還要跳出互動視窗 (Modal) 來警告使用者。
第二十九行:<Counter {count} on:changeCount={handleClick}/>
如同在第 13 天當中實作的,把 count
作為 Property 傳入 <Counter>
,並加上事件處理器 handleClick
處理 changeCount
客製化事件。
第三十四行:<Modal {showModal} on:closeModal={() => showModal = false}/>
加入 <Modal>
,並將 showModal
作為 Property 傳入 <Modal>
,並加上行內 (inline) 事件處理器 () => showModal = false
,當街收到來自 <Modal>
的 closeModal
事件,就將 showModal
這個變數重新賦值為 false
。
圖一、count
只能到 87
,不能再高了
根據以上程式碼,我們能夠做出一個不允許 count
超過 87
,並且在 count
即將超過 87
時,開啟互動視窗 (Modal) 提醒使用者的奇妙專案了。已經開啟的互動視窗 (Modal) 則可以藉由按下鍵盤【Esc】鍵,或是點擊互動視窗 (Modal) 當中的 <button>
來關閉。
但我們平常使用的互動視窗 (Modal),通常還有另一種關閉的作法,那就是點擊互動視窗 (Modal) 外的空白處,如圖二。
圖二、常見關閉互動視窗的手段
要怎麼做到這件事呢?其實只要讓 <dialog>
能夠接收點擊事件,並且同樣加上事件處理器 handleClose
去關閉互動視窗 (Modal) 就好。也就是讓 <dialog>
變成這樣:
<dialog on:close={handleClose} on:click={handleClose}>
為什麼這樣能行呢?因為互動視窗 (Modal) 雖然看起來只有中間那個對話框,但實際上是藉由 CSS 的 margin 占滿整個畫面的,如圖三。所以雖然說是點擊互動視窗外的空白處,但實際上還是點在 <dialog>
這個元素上面。
圖三、看看 <dialog>
廣大的 margin
但這樣做其實會有一個明顯的問題,那就是點擊互動視窗的對話框本身也會觸發點擊 (click
) 事件,進而呼叫事件處理器 handleClose
,導致互動視窗也被關閉起來了。這就不是我們希望看到的了。該怎麼避免呢?
self
很簡單,還記得我們昨天討論過的事件修飾,其中一個修飾詞 self
,限制事件必須發生在指定的 HTML 元素本身,符合條件才會呼叫事件處理器。所以只要在 on:click
加上我們的事件修飾 self
:
<dialog on:close={handleClose} on:click|self={handleClose}>
並且記得用 CSS 把 <dialog>
內的 HTML 元素做成如圖四,也就是 <dialog>
元素本身沒有 padding,而由 <dialog>
內 HTML 元素的 margin 來做出想要的空間感。如此一來,點擊互動視窗的對話框,其 event.target
只能是 <dialog>
內的 HTML 元素,而不會是 <dialog>
本人,就不符合 on:click|self
的條件,就不會呼叫 handleClose
了。
圖四、用 <dialog>
內的 HTML 元素占滿整個對話框
stopPropagation
用事件修飾詞 self
可以完美達成我們的需求。用 stopPropagation
也同樣可以。既然我們昨天學了這麼多事件修飾詞,就來嘗試看看不同修飾詞的功能吧。stopPropagation
的用法是,阻止發生在 HTML 元素的事件繼續往更高一層的 HTML 元素傳遞 (bubble)。所以用 stopPropagation
的邏輯,是藉由在 <dialog>
內的 HTML 元素放置一個 on:click|stopPropagation
,來把所有點擊在互動視窗對話框的事件都攔下來:
<dialog on:close={handleClose} on:click={handleClose}>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:click|stopPropagation>
<!-- 在這邊省略顯示內部的 HTML 元素 -->
</div >
</dialog>
圖五、一個完美的 87 分守門員
好的~我們今天終於把互動視窗給完成了,並且也實際練習了兩個事件修飾詞的使用。今天的內容就到這邊了,關於最後實作完成的互動視窗程式碼可以看文末的附錄段落,或是到 Github 的資源庫看完整程式碼。謝謝各位讀者。
self
實作互動視窗/src/lib/Modal.svelte
<script>
import { createEventDispatcher } from "svelte";
export let showModal;
let dialog;
const dispatch = createEventDispatcher();
const handleClose = () => dispatch('closeModal');
setTimeout(() => {
dialog = document.querySelector('dialog');
// 刪除預設開啟的程式碼 dialog.showModal();
}, 1);
$: if (dialog && showModal) dialog.showModal();
$: if (dialog && !showModal) dialog.close();
</script>
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
<dialog on:close={handleClose} on:click|self={handleClose}>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div>
<div class="title">
<h2>Forbidden</h2>
<small><em>adjective</em> for·bid·den \fər-ˈbi-dᵊn \</small>
</div>
<hr />
<div class="content">
<p>
You have no privilidge to enter domain beyond 87. Please register
as member so we can sent you some spam email.
</p>
</div>
<hr />
<!-- svelte-ignore a11y-autofocus -->
<div class="footer">
<button autofocus on:click={handleClose}>close modal</button>
</div>
</div>
</dialog>
<style>
dialog {
color: var(--color-text);
max-width: 20em;
border-radius: 0.2em;
border: none;
margin: auto;
box-shadow: 0.5em 0.5em 1.5em rgba(0, 0, 0, 0.1),
-0.5em -0.5em 1.5em rgba(0, 0, 0, 0.1);
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.4);
}
dialog > div {
padding: 2em;
}
dialog > div > div {
margin: 1em 0;
}
button {
font-size: 1.1em;
padding: 0.5em;
border-radius: 0.2em;
border: none;
outline: none;
cursor: pointer;
}
button:hover {
filter: brightness(1.02);
}
</style>
stopPropagation
實作互動視窗/src/lib/Modal.svelte
<script>
import { createEventDispatcher } from "svelte";
export let showModal;
let dialog;
const dispatch = createEventDispatcher();
const handleClose = () => dispatch('closeModal');
setTimeout(() => {
dialog = document.querySelector('dialog');
// 刪除預設開啟的程式碼 dialog.showModal();
}, 1);
$: if (dialog && showModal) dialog.showModal();
$: if (dialog && !showModal) dialog.close();
</script>
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
<dialog on:close={handleClose} on:click={handleClose}>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:click|stopPropagation>
<div class="title">
<h2>Forbidden</h2>
<small><em>adjective</em> for·bid·den \fər-ˈbi-dᵊn \</small>
</div>
<hr />
<div class="content">
<p>
You have no privilidge to enter domain beyond 87. Please register
as member so we can sent you some spam email.
</p>
</div>
<hr />
<!-- svelte-ignore a11y-autofocus -->
<div class="footer">
<button autofocus on:click={handleClose}>close modal</button>
</div>
</div>
</dialog>
<style>
dialog {
color: var(--color-text);
max-width: 20em;
border-radius: 0.2em;
border: none;
margin: auto;
box-shadow: 0.5em 0.5em 1.5em rgba(0, 0, 0, 0.1),
-0.5em -0.5em 1.5em rgba(0, 0, 0, 0.1);
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.4);
}
dialog > div {
padding: 2em;
}
dialog > div > div {
margin: 1em 0;
}
button {
font-size: 1.1em;
padding: 0.5em;
border-radius: 0.2em;
border: none;
outline: none;
cursor: pointer;
}
button:hover {
filter: brightness(1.02);
}
</style>